上一章節簡單的介紹如何處理 side effect 的其中一個方法 dependency injection,而本章要介紹第二個方法 IO Monad,但在這之前要先了解什麼是 thunk!
In computer programming, a thunk is a subroutine used to inject a calculation into another subroutine. Thunks are primarily used to delay a calculation until its result is needed, or to insert operations at the beginning or end of the other subroutine. They have many other applications in compiler code generation and modular programming. -wiki
簡單來說, thunk 就是一個函式其包覆一個表示式,其目的就是為了延遲運算。
thunk :: () -> a
而延遲運算跟 side effect 有什麼關係呢?
example
const secret = () => {
console.log('hi, :))))');
return 18;
}
現在有一個 secret
的函式,可以看到這個函式是 impure function,因為在呼叫的同時,也在 console 寫入一些東西,但又必須知道這個 secret
內的值才能進行接續的動作,有什麼方式讓 secret
既是 pure function 也可以在呼叫後接續其他動作
統整一下我們現在要做的事
secret
, 從 impure 到 pure function改寫 secret
, 從 impure 到 pure function
沒錯,這時候 thunk 的概念就派上用場了
const secretThunk = () => {
const secret = () => {
console.log('hi, :))))');
return 18;
}
return secret;
}
現在 secretThunk
已經是一個純函式了,給定一樣的輸入就會回傳一樣的輸出,並且無任何 side effect.
secretThunk() // f
secretThunk() // f
secretThunk() // f
用 secret number 繼續進行運算
如果現在我們得到了 secret number,還想要再進行其他動作呢? 例如將其乘 2
const calcSecretNumber = (f) => {
return f() * 2;
}
calcSecretNumber(secret)
// hi, :))))
// 36
但如果還有其他動作呢?? 這樣不就產生了 side effect 了嗎? 所以也用 thunk 包住其運算
const calcSecretNumberThunk = (f) => {
return () => f() * 2;;
}
const doubleSecret = calcSecretNumberThunk(secret)
const quadrupleSecret = calcSecretNumberThunk(doubleSecret)
quadrupleSecret()
可以看到我們將 side effect 的程式碼,透過 thunk 這個概念,將其延遲執行,直到需要時在呼叫。這就像是劃分了一條界線,將 impure 跟 pure 切分開來,我們無法避免 side effect,但可以將其封裝在一個類似盒子裡,並在需要的時候在打開,而這個概念就延伸出 IO Monad, 但我們總不會想要每次都要將 impure function 再包一層,讓其變成 pure,或是把每個相關用到的函式都套用 thunk。
所以來介紹 IO Monad 吧!
Constructor
IO :: () -> a
沒錯 IO Monad 的 type signature 跟 thunk 基本上概念是一樣的,就是包覆住該表示式,而剛剛提到要如何再取得 secret number 後運算,其實就是 IO functor 在做的事
const IO = run => ({
run,
map: (f) => IO(() => f(run()))
})
IO.of = (x) => IO(() => x);
改寫前述範例
const secret = () => {
console.log('hi, :))))');
return 18;
};
const calcSecretNumber = (num) => num * 2;
const effect = IO(secret)
.map(calcSecretNumber)
.map(calcSecretNumber)
effect.run()
// hi, :))))
// 72
可以看到 calcSecretNumber
就是一般函式,不用在進行任何加工,就如同前面所提到的,當需要執行這個 side effect 的程式時,只需要呼叫 run
就好,而在這之前此程式都不會有任何動作。
如果今天是 effect 跟 effect 要進行 compose 呢?? IO Monad 的 chain
該如何實作
const IO = run => ({
run,
map: (f) => IO(() => f(run())),
chain: (f) => IO(() => f(run()).run()),
})
而 IO 的 chain
method 其實也很簡單,邏輯跟 map
基本上是一樣的,只不過多了打平,像是要將 IO(IO(a))
打平,方法就是呼叫一次 run,這樣結構就會變成 IO(a)
舉例,現在有另一個 IO otherEffect
const otherEffect = (num) => IO.of(R.add(10, num));
此時要將兩個 IO 做 compose,就可以用 chain
const effect = IO(secret)
.map(calcSecretNumber)
.map(calcSecretNumber)
.chain(otherEffect);
effect.run()
// hi, :))))
// 82
const IO = run => ({
run,
map: (f) => IO(() => f(run())),
ap: eff => eff.map(effRun => effRun(run())),
chain: (f) => IO(() => f(run()).run()),
})
const lift2 = R.curry((g, f1, f2) => f2.ap(f1.map(g)))
而 ap
也不用多加贅述,想必各位讀者都非常熟悉了! 也就是將兩個以上的 IO 跟函式進行結合!
const result = lift2(R.add, IO.of(1), IO.of(1))
result.run() // 2
以下提供 point-free version 給大家參考
IO Monad
const of = a => () => a;
const map = (run) => (fb) => () => run(fb())
const chain = (run) => (mb) => () => run(mb())()
const ap = (run) => (f2) => () => {
const f = f2();
const a = run();
return f(a)
}
example
const otherEffect = (num) => of(R.add(10, num));
const pipe = (init, ...fns) =>
fns.reduce((prevValue, fn) => fn(prevValue), init);
const effect = pipe(
secret,
map(calcSecretNumber),
map(calcSecretNumber),
chain(otherEffect)
);
effect();
// hi, :))))
// 82
之後再實作篇,也會有 IO Monad 的實際案例,而今天就是帶讀者們了解概念!
感謝大家的閱讀
NEXT: Either Monad